package ch.ethz.inf.vs.sunspot.drivercore;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import ch.ethz.inf.vs.sunspot.Constants;
import ch.ethz.inf.vs.sunspot.commons.Utilities;

/**
 * The main class responsible for Managing the Sun SPOTs and the Driver itself
 * 
 * @author Simon Mayer, ETH Zurich
 */
public class SpotManagement {

	// Logging instance
	private static Logger log = Logger.getLogger("SpotManagement");

	// Static because it should remain over multiple restarts of the Management instance
	private static ConcurrentSkipListSet<String> blockedSpots;

	/** Map of connected spots. */
	public ConcurrentMap<String, SpotManagementRepresentation> connectedSpots;
	private ConcurrentMap<String, SpotManagementRepresentation> toberemovedSpots;

	// Class responsible for periodically testing the SPOT connectivities
	private ConnectivityTesterThread connectivityTester;

	// Internal Management of the ports to Sun SPOTs
	private ConcurrentSkipListSet<Integer> portList = null;

	private List<String> superUserNames;
	private List<String> currentSuperusers;

	// Benchmarking information
	private Integer removeEvents = 0;
	/** Number of spots. */
	public Integer totalSpots = 0;

	/** Is the Management active? */
	public Boolean isActive = false;

	/**
	 * Returns whether or not a superuser is present.
	 * @return Whether or not a superuser is present.
	 */
	public Boolean isSuperuserPresent() {
		return (!currentSuperusers.isEmpty());
	}

	/**
	 * Create a new spot and its required management structures.
	 * @param spotName - The name of the new spot.
	 * @param spotAddress - The address of the new spot.
	 * @return The spot port.
	 */
	public Integer createNewSpot(String spotName, String spotAddress) {
		// Determine if Superuser
		if (spotName.startsWith(SpotRepresentation.USER_TOKEN)) {
			String userName = spotName.substring(SpotRepresentation.USER_TOKEN.length());
			if(superUserNames.contains(userName)) {
				currentSuperusers.add(userName);
			}
		}

		// If a SPOT is in the toberemovedSpots-list, there is already a Thread for it...
		if (SpotManagement.getInstance().toberemovedSpots.containsKey(spotName)) {
			log.log(Level.INFO, "Spot is CONTAINED in toberemovedSpots");
			Integer spotPort = this.getNextFreePort();

			// Catch Spots that return after a short period of downtime (via toberemovedSpots)
			SpotManagementRepresentation newSpot = SpotManagement.getInstance().toberemovedSpots.get(spotName);

			newSpot.port = spotPort;
			addPortToPortList(spotPort);
			SpotManagement.getInstance().toberemovedSpots.remove(spotName);
			
			SpotManagement.getInstance().connectedSpots.put(spotName, newSpot);
			SpotManagement.getInstance().setTimeSinceLastSync(spotName, 0);						
			newSpot.start();
			newSpot.asJSON.setReload();

			return spotPort;
		}		
		else if (!SpotManagement.getInstance().connectedSpots.containsKey(spotName)) {
			Integer spotPort = this.getNextFreePort();
			
			log.log(Level.INFO, "Spot is NOT CONTAINED in connectedSpots");

			// Catch Spots that have the same name as already connected ones
			SpotManagementRepresentation newSpot = new SpotManagementRepresentation(spotName, spotAddress, spotPort);

			addPortToPortList(spotPort);
			SpotManagement.getInstance().connectedSpots.put(spotName, newSpot);

			return spotPort;
		}
		else if (SpotManagement.getInstance().connectedSpots.containsKey(spotName)) {
			log.log(Level.INFO, "Spot IS CONTAINED in connectedSpots. Returning port " + SpotManagement.getInstance().connectedSpots.get(spotName).port);

			return SpotManagement.getInstance().connectedSpots.get(spotName).port;
		}
		else {
			log.log(Level.INFO, "Something Else in CreateSpot");
			return null;
		}
	}

	/**
	 * Initializes removal of a sun spot. The spot keeps to be available for Constants.SPOT_SYNC_MAXINTERVAL more ms.
	 * @param spotName - The name of the spot to be removed.
	 */
	public void removeSpot(String spotName) {
		if(spotName.startsWith(SpotRepresentation.USER_TOKEN)) {
			String userName = spotName.substring(SpotRepresentation.USER_TOKEN.length());
			if(superUserNames.contains(userName)) currentSuperusers.remove(userName);
		}
		if(SpotManagement.getInstance().connectedSpots.containsKey(spotName) && !SpotManagement.getInstance().toberemovedSpots.containsKey(spotName)) {
			SpotManagement.getInstance().getSpot(spotName).destroy();

			removeEvents++;
			log.log(Level.FINE, "REMOVE EVENTS: " + removeEvents);

			// SpotManagementRepresentation is NOT deleted (gc)!
			removePortFromPortList(SpotManagement.getInstance().connectedSpots.get(spotName).port);
			
			SpotManagement.getInstance().toberemovedSpots.put(spotName, SpotManagement.getInstance().connectedSpots.get(spotName));			
			SpotManagement.getInstance().connectedSpots.remove(spotName);
			
			new SpotRemoverThread(spotName);
		}
	}

	/**
	 * Initializes removal of a sun spot. The spot keeps to be available for Constants.SPOT_SYNC_MAXINTERVAL more ms.
	 * @param spotName - The name of the spot to be removed.
	 */
	public void removeSpotImmediately(String spotName) {
		if(SpotManagement.getInstance().connectedSpots.containsKey(spotName) && !SpotManagement.getInstance().toberemovedSpots.containsKey(spotName)) {
			SpotManagement.getInstance().getSpot(spotName).destroy();

			removeEvents++;
			log.log(Level.FINE, "REMOVE EVENTS: " + removeEvents);

			// SpotManagementRepresentation is NOT deleted (gc)!
			SpotManagement.getInstance().connectedSpots.remove(spotName);
		}
	}

	/**
	 * Determine a free port to connect a new Sun SPOT.
	 * @return The next free port.
	 */
	public Integer getNextFreePort() {
		// Choose starting port u.a.r.
		Integer nextPort = Utilities.nextRandomInteger(254) + 1;
		do {
			nextPort = (nextPort + 1) % 255; 
		} while (portList.contains(nextPort));
		return nextPort;
	}

	/**
	 * Deferred removing of Sun SPOTs.
	 */
	class SpotRemoverThread implements Runnable {
		private Thread t;

		private boolean hasStarted = false;
		String spotName;

		/**
		 * Create and start a new Spot Remover Thread.
		 * @param spotName - The name of the SPOT to be removed.
		 */
		public SpotRemoverThread(String spotName) {
			t = new Thread(this, "Sun SPOT Remover");
			this.spotName = spotName;
			if(!this.hasStarted) t.start();
		}

		@Override
		public void run() {
			hasStarted = true;

			log.fine("Preparing removal of spot " + spotName);
			try {
				Thread.sleep(Constants.SPOT_SYNC_MAXINTERVAL);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			// If the spot still is to be removed, remove it
			if (SpotManagement.getInstance().toberemovedSpots.containsKey(spotName)) {
				log.log(Level.INFO, "Spot " + spotName + " that is to be removed was REMOVED!");
				
				SpotManagement.getInstance().toberemovedSpots.remove(spotName);
			}
		}
	}

	private void addPortToPortList(int port) {
		log.log(Level.INFO, "Adding port to portlist: " + port);
		SpotManagement.getInstance().portList.add(port);
	}
	
	private void removePortFromPortList(int port) {
		log.log(Level.INFO, "Removing port from portlist: " + port);
		SpotManagement.getInstance().portList.remove(port);
	}
	
	/**
	 * Singleton of the class
	 */
	private static SpotManagement INSTANCE = new SpotManagement();

	/**
	 * Delivers the Singleton
	 * @return the instance
	 */
	public static SpotManagement getInstance() {
		return INSTANCE;
	}	

	/**
	 * Private Constructor (Singleton)
	 */
	private SpotManagement() {
		connectedSpots = new ConcurrentHashMap<String, SpotManagementRepresentation>();
		toberemovedSpots = new ConcurrentHashMap<String, SpotManagementRepresentation>();
		connectivityTester = new ConnectivityTesterThread();
		superUserNames = new ArrayList<String>();
		superUserNames.add("mayersi");
		currentSuperusers = new ArrayList<String>();

		// This remains static vs. Management restarts
		if (blockedSpots == null) blockedSpots = new ConcurrentSkipListSet<String>();

		portList = new ConcurrentSkipListSet<Integer>();
		portList.add(0);
		portList.add(Constants.SPOT_HOST_PORT);
		isActive = true;
		log.setLevel(Constants.LOGLEVEL);
	}

	/**
	 * Restart the Management / The Driver
	 */
	public void restart() {
		log.info("Booting up Management Instance!");
		SpotManagement.getInstance().stop();
		INSTANCE = new SpotManagement();
	}

	/**
	 * Stop the Management / The Driver
	 */
	public void stop() {
		log.info("Shutting down Management Instance!");
		destroySpots();
		connectivityTester.stop();
		isActive = false;
	}

	/**
	 * Remove all SPOTs and their corresponding Management structures
	 */
	private void destroySpots() {
		for (String spotName : connectedSpots.keySet()) {
			SpotManagement.getInstance().removeSpotImmediately(spotName);
		}
	}

	/**
	 * Class responsible for periodically testing the SPOT connectivities
	 */
	private class ConnectivityTesterThread implements Runnable {

		private Thread t;
		private boolean hasStarted = false;
		private boolean keepRunning = true;

		/**
		 * Create a new Connectivity Tester Thread
		 */
		public ConnectivityTesterThread() {
			t = new Thread(this, "Sun SPOT Connectivity Tester");
			this.keepRunning = true;
			if(!this.hasStarted) t.start();
		}

		public void stop() {
			keepRunning = false;
		}

		@Override
		public void run() {
			while(keepRunning) {
				this.testConnectivities();

				try {
					Thread.sleep(Constants.SPOT_CONNECTIVITY_INTERVAL);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}			
		}	

		/**
		 * For every connected spot, test if it hasn't violated its timeout
		 */
		private void testConnectivities() {
			for (String spotName : SpotManagement.getInstance().connectedSpots.keySet()) {
				SpotManagementRepresentation currentSpot = SpotManagement.getInstance().getSpot(spotName);			

				if (currentSpot.timeSincelastSync > (currentSpot.currentSyncTimeout * (1 + Constants.SPOT_SYNC_TOLERANCE_REL)) && currentSpot.timeSincelastSync > (currentSpot.currentSyncTimeout + Constants.SPOT_SYNC_TOLERANCE_ABS)) {
					log.log(Level.INFO, "Removing (" + spotName + " after " + currentSpot.timeSincelastSync + "ms of inactivity)");
					SpotManagement.getInstance().removeSpot(spotName);				
				}
				else {
					// This is going to accumulate errors as the Constants.SPOT_SYNC_RECEIVETIMEOUT is only an approximation of the actual time the function takes.
					// As long as these are smaller than the Constants.SPOT_SYNC_RECEIVETIMEOUT, it's ok!
					// Errors are 0-ed at every sync!
					// Constants.SPOT_SYNC_TOLERANCE also counteracts this!
					currentSpot.timeSincelastSync = currentSpot.timeSincelastSync + Constants.SPOT_CONNECTIVITY_INTERVAL;
				}				
			}
		}
	}

	/**
	 * Prints out management information (contained in SpotManagementRepresentation) of all connected Spots
	 */
	public void printSpotManagementInformation() {
		log.log(Level.FINE, "---------------------------------------------------------");
		if(SpotManagement.getInstance().connectedSpots.size() == 0) log.fine("No Spots Connected!");
		for (String spotName : SpotManagement.getInstance().connectedSpots.keySet()) {
			SpotManagementRepresentation currentSpot = SpotManagement.getInstance().getSpot(spotName);			

			log.log(Level.FINE, "Management information on Spot " + spotName + "...");
			log.log(Level.FINE, "\tAddress:\t" + currentSpot.address);
			log.log(Level.FINE, "\tLast Contact:\t" + currentSpot.timeSincelastSync + " ms");

			Integer relTimeout = (int) (currentSpot.currentSyncTimeout * (1 + Constants.SPOT_SYNC_TOLERANCE_REL));
			Integer absTimeout = (int) (currentSpot.currentSyncTimeout + Constants.SPOT_SYNC_TOLERANCE_ABS);
			if (relTimeout > absTimeout) absTimeout = relTimeout;

			log.log(Level.FINE, "\tTimeout:\t" + currentSpot.currentSyncTimeout + " ms [" + absTimeout + " ms]");			
			log.log(Level.FINE, "\tFuture Timeout:\t" + currentSpot.futureSyncTimeout + " ms");
			log.log(Level.FINE, "\tLast Call:\t" + (System.currentTimeMillis() - currentSpot.timeOfLastCall) + " ms");
		}

		log.log(Level.FINE, "---------------------------------------------------------");
	}

	/**
	 * @param spotName
	 * @return whether the spot with the given name exists or not
	 */
	public Boolean spotExists(String spotName) {
		return isSpotConnected(spotName) || isSpotDataAvailable(spotName);
	}

	/**
	 * @param spotName
	 * @return whether the spot with the given name is connected
	 */
	public Boolean isSpotConnected(String spotName) {
		return SpotManagement.getInstance().connectedSpots.containsKey(spotName);
	}

	/**
	 * @param spotName
	 * @return whether data is available for the spot with the given name
	 */
	public Boolean isSpotDataAvailable(String spotName) {
		return SpotManagement.getInstance().toberemovedSpots.containsKey(spotName);
	}

	/**
	 * Sets the sync timout default.
	 * @param newInterval
	 */
	public void setSyncTimeoutDefault(int newInterval) {
		Constants.SPOT_SYNC_INTERVAL = newInterval;
	}


	/**
	 * @return the blocked spots
	 */
	public ConcurrentSkipListSet<String> getBlockedSpots() {
		return blockedSpots;
	}

	/**
	 * @param spotName
	 */
	public void addBlockedSpot(String spotName) {
		blockedSpots.add(spotName);
		this.removeSpotImmediately(spotName);
	}

	/**
	 * @param spotName
	 */
	public void removeBlockedSpot(String spotName) {
		blockedSpots.remove(spotName);
	}

	/**
	 * @return the set of port numbers
	 */
	public ConcurrentSkipListSet<Integer> getPortNumbers() {
		return this.portList;
	}

	/**
	 * @param spotName
	 * @return the time of the last call
	 */
	public Long getTimeOfLastCall(String spotName) {
		return SpotManagement.getInstance().getSpot(spotName).timeOfLastCall;
	}

	/**
	 * @param spotName
	 * @param newTime
	 */
	public void setTimeOfLastCall(String spotName, Long newTime) {
		SpotManagement.getInstance().getSpot(spotName).timeOfLastCall = newTime;
	}

	/**
	 * Set a new synchronization timeout for the SPOT spotName
	 * @param spotName
	 * @param newTimeout
	 */
	public void setFutureSyncTimeout(String spotName, Integer newTimeout) {
		log.log(Level.FINE, "Requested Timeout: " + newTimeout);

		if (newTimeout > Constants.SPOT_SYNC_MAXINTERVAL) {
			log.log(Level.FINE, "Requested synchronization interval (" + newTimeout + "ms) too large. Set to " + Constants.SPOT_SYNC_MAXINTERVAL);
			newTimeout = Constants.SPOT_SYNC_MAXINTERVAL;			
		}
		else if (newTimeout < Constants.SPOT_SYNC_MININTERVAL) {
			log.log(Level.FINE, "Requested synchronization interval (" + newTimeout + "ms) too small. Set to " + Constants.SPOT_SYNC_MININTERVAL);
			newTimeout = Constants.SPOT_SYNC_MININTERVAL;
		}

		// Set new timeout
		SpotManagement.getInstance().getSpot(spotName).futureSyncTimeout = newTimeout;
	}

	/**
	 * @param spotName
	 * @return the {@link SpotRepresentation} of a certain spot
	 */
	public SpotRepresentation getSpotRepresentation(String spotName) {
		if (spotExists(spotName)) return SpotManagement.getInstance().getSpot(spotName).asJSON;
		else return null;
	}

	private SpotManagementRepresentation getSpot(String spotName) {
		if (isSpotConnected(spotName)) {
			return SpotManagement.getInstance().connectedSpots.get(spotName);
		}
		else if (isSpotDataAvailable(spotName)) {
			return SpotManagement.getInstance().toberemovedSpots.get(spotName);
		}
		else return null;
	}

	/**
	 * @param spotName
	 * @param newRep
	 */
	public void setRepresentation(String spotName, SpotRepresentation newRep) {
		SpotManagement.getInstance().getSpot(spotName).asJSON = newRep;
	}

	/**
	 * @param spotName
	 * @return the time since the last sync
	 */
	public Integer getTimeSinceLastSync(String spotName) {
		return SpotManagement.getInstance().getSpot(spotName).timeSincelastSync;
	}

	/**
	 * @param spotName
	 * @return the future sync timeout
	 */
	public Integer getFutureSyncTimeout(String spotName) {
		return SpotManagement.getInstance().getSpot(spotName).futureSyncTimeout;
	}

	/**
	 * @param spotName
	 * @param newTimeout
	 */
	public void setSyncTimeout(String spotName, Integer newTimeout) {
		SpotManagement.getInstance().getSpot(spotName).currentSyncTimeout = newTimeout;
	}

	/**
	 * @param spotName
	 * @param newAnsweringTime
	 */
	public void setTimeSinceLastSync(String spotName, Integer newAnsweringTime) {
		SpotManagement.getInstance().getSpot(spotName).timeSincelastSync = newAnsweringTime;
	}

	/**
	 * @param spotName
	 * @return the spot address for a certain spot
	 */
	public String getSpotAddress(String spotName) {
		return SpotManagement.getInstance().getSpot(spotName).address;
	}

	/**
	 * @param spotName
	 * @return the spot port for a certain spot
	 */
	public Integer getSpotPort(String spotName) {
		return SpotManagement.getInstance().getSpot(spotName).port;
	}

	/**
	 * Class to represent a Spot internally
	 * @author mayersi
	 *
	 */
	private class SpotManagementRepresentation {
		public String name;
		public String address;
		public Integer port;
		public Integer timeSincelastSync;
		public Integer currentSyncTimeout;
		public Integer futureSyncTimeout;
		public Long timeOfLastCall;
		private SpotSynchronizerThread syncer;
		public SpotRepresentation asJSON;

		public SpotManagementRepresentation(String spotName, String spotAddress, Integer spotPort) {
			this.name = spotName;
			this.address = spotAddress;
			this.port = spotPort;
			this.timeSincelastSync = 0;
			this.currentSyncTimeout = Constants.SPOT_SYNC_INTERVAL;
			this.futureSyncTimeout = Constants.SPOT_SYNC_INTERVAL;
			this.timeOfLastCall = System.currentTimeMillis();
			this.asJSON = new SpotRepresentation(spotName);

			start();
		}

		public void destroy() {
			log.info("Syncer of spot " + name + " shutting down!");
			this.syncer.stop();
		}

		public void start() {
			this.syncer = new SpotSynchronizerThread(this.port, this.name, this.address);
		}
	}

	/** BENCHMARKING information **/

	/**
	 * Benchmarking information.
	 * @return the number of remove events
	 */
	public Integer getNumberRemoveEvents() {
		return removeEvents;
	}

	/**
	 * Benchmarking information.
	 * @return the number of sync operations
	 */
	public Integer getNumberSyncOperations() {
		return totalSpots;
	}

	/**
	 * @param newSpotHostPort
	 */
	public void setSpotHostPort(Integer newSpotHostPort) {
		Constants.SPOT_HOST_PORT = newSpotHostPort;
	}


}